Skip to content

feat(billing): hygiene polish — stack trace, req.id, $slice ledger, TTL archived, cache utils#3614

Merged
PierreBrisorgueil merged 2 commits into
masterfrom
feat/billing-polish-final
May 6, 2026
Merged

feat(billing): hygiene polish — stack trace, req.id, $slice ledger, TTL archived, cache utils#3614
PierreBrisorgueil merged 2 commits into
masterfrom
feat/billing-polish-final

Conversation

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor

Summary

Étape 5 — final polish layer for billing prod-ready (devkit Node only). Builds on Étapes 1–4 (#3611 #3612 #3613).

A — Dead-letter observability + req.id correlation

  • incrementAttempts now stores err.stack (full trace) rather than just err.message; JSON-serializes plain-object throws to avoid [object Object]
  • withIdempotency accepts optional ctx.requestId; logs handler entry / skip / retry / dead-letter with the request ID for end-to-end traceability
  • Webhook controller reads req.id (injected by global requestId middleware) and propagates it to all withIdempotency calls

B — Performance hygiene

  • listLedger refactored to use new listLedgerPage() repo method — MongoDB aggregation $sortArray + $slice transfers only the requested page over the wire instead of the full document
  • Perf integration test asserts < 50ms on a 1000-entry ledger page fetch
  • BillingUsage: TTL index on archivedAt (expireAfterSeconds: 365 days, partialFilterExpression: { archivedAt: { $type: 'date' } }) — auto-purge archived weekly periods after 1 year; active docs (archivedAt null/absent) are excluded

C — Tier-3 nits

  • Removed module-scope METER_RUN_BASE const (evaluated stale at load time); getMeterRunBase() function kept and re-exported from billing.meter.service.js for lazy evaluation
  • Exported clearPlansCache() from billing.plans.service.js for test isolation (resets all module-scope cache state between test files)
  • fetchPlansFromStripe: explicit warn log when a Stripe price ID is mapped to multiple plans — was previously silent (config error causing billing misrouting)

Pre-push review

DeepSeek critical review verdict: OK with nits — 0 critical, 0 high. Two nits addressed:

  • Removed redundant $exists: true from TTL partialFilterExpression (kept $type: 'date' which is sufficient)
  • Improved lastError serialization for non-Error object throws (JSON.stringify fallback)

Test plan

  • All 1245 unit tests green (npm run test:unit)
  • New perf integration test: billing.extraBalance.listLedger.perf.integration.tests.js — asserts < 50ms on 1000-entry ledger
  • Existing webhook idempotency tests updated to verify full Error passed to incrementAttempts
  • Plans service tests: clearPlansCache export + duplicate price warn assertions
  • CI green incl. codecov

…TL archived, cache utils

Étape 5 — Polish (devkit-only):

A — lastError + req.id correlation
- incrementAttempts stores err.stack (not just message); JSON fallback for plain-object throws
- withIdempotency accepts optional ctx.requestId; logs entry/skip/retry/dead-letter
- Webhook controller reads req.id and threads it into all withIdempotency calls

B — Performance hygiene
- listLedger: new listLedgerPage() repo method uses $slice aggregation — only the page
  is transferred over the wire; perf integration test asserts < 50ms on 1000 entries
- BillingUsage: TTL index on archivedAt (1-year, partialFilterExpression $type date)
  so archived weekly periods are auto-purged without touching active docs

C — Tier 3 nits
- Removed module-scope METER_RUN_BASE const (stale at load time); getMeterRunBase()
  function kept and re-exported from billing.meter.service.js
- Exported clearPlansCache() from billing.plans.service.js for test isolation
- fetchPlansFromStripe: explicit warn log when Stripe price ID mapped to multiple plans
Copilot AI review requested due to automatic review settings May 6, 2026 19:15
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@PierreBrisorgueil has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 33 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1d794072-56fd-4adf-b277-bfec7dc148f6

📥 Commits

Reviewing files that changed from the base of the PR and between f81aa78 and 84d429c.

📒 Files selected for processing (17)
  • modules/billing/controllers/billing.webhook.controller.js
  • modules/billing/lib/billing.constants.js
  • modules/billing/models/billing.usage.model.mongoose.js
  • modules/billing/repositories/billing.extraBalance.repository.js
  • modules/billing/repositories/billing.processedStripeEvent.repository.js
  • modules/billing/services/billing.extra.service.js
  • modules/billing/services/billing.meter.service.js
  • modules/billing/services/billing.plans.service.js
  • modules/billing/services/billing.webhook.service.js
  • modules/billing/tests/billing.controller.unit.tests.js
  • modules/billing/tests/billing.extra.service.unit.tests.js
  • modules/billing/tests/billing.extraBalance.listLedger.perf.integration.tests.js
  • modules/billing/tests/billing.meter.service.unit.tests.js
  • modules/billing/tests/billing.plans.unit.tests.js
  • modules/billing/tests/billing.processedStripeEvent.repository.unit.tests.js
  • modules/billing/tests/billing.webhook.hardening.unit.tests.js
  • modules/billing/tests/billing.webhook.idempotency.unit.tests.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/billing-polish-final

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.34%. Comparing base (f81aa78) to head (84d429c).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3614      +/-   ##
==========================================
+ Coverage   88.17%   88.34%   +0.16%     
==========================================
  Files         134      134              
  Lines        4432     4462      +30     
  Branches     1366     1380      +14     
==========================================
+ Hits         3908     3942      +34     
+ Misses        411      408       -3     
+ Partials      113      112       -1     
Flag Coverage Δ
integration 59.43% <19.60%> (-0.29%) ⬇️
unit 62.37% <88.23%> (+0.20%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f81aa78...84d429c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR applies a “production hygiene” polish to billing by improving webhook observability (stack traces + requestId correlation), reducing ledger list payloads via server-side slicing, and making a few test/ops-oriented refinements (TTL purge for archived usage, cache reset utilities, and config resolution lazily).

Changes:

  • Webhook idempotency: persist full error stack traces, add requestId correlation, and update controller + tests accordingly.
  • Ledger pagination: introduce listLedgerPage() using Mongo aggregation $sortArray + $slice, update service + add a perf integration test.
  • Billing maintenance utilities: TTL for archived usage docs, export clearPlansCache(), swap stale module-scope meter constant for lazy getMeterRunBase().

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
modules/billing/services/billing.webhook.service.js Adds requestId-aware logging and passes full Error objects into attempt tracking.
modules/billing/controllers/billing.webhook.controller.js Propagates req.id into webhook processing and standardizes log payloads.
modules/billing/repositories/billing.processedStripeEvent.repository.js Stores stack traces / robust error serialization in incrementAttempts.
modules/billing/repositories/billing.extraBalance.repository.js Adds listLedgerPage() aggregation to fetch only a ledger slice.
modules/billing/services/billing.extra.service.js Switches listLedger to repository pagination with safe page/limit handling.
modules/billing/services/billing.plans.service.js Adds duplicate priceId warning + exports clearPlansCache() for test isolation.
modules/billing/models/billing.usage.model.mongoose.js Adds TTL index to purge archived usage docs after 1 year.
modules/billing/lib/billing.constants.js Removes eager METER_RUN_BASE constant; keeps lazy getMeterRunBase().
modules/billing/services/billing.meter.service.js Stops exporting constant and re-exports getMeterRunBase on the facade.
modules/billing/tests/billing.webhook.idempotency.unit.tests.js Updates assertions for Error-object attempt tracking + requestId ctx acceptance.
modules/billing/tests/billing.webhook.hardening.unit.tests.js Updates attempt tracking assertion to expect Error objects.
modules/billing/tests/billing.processedStripeEvent.repository.unit.tests.js Adds coverage for stack trace capture and serialization fallbacks.
modules/billing/tests/billing.extra.service.unit.tests.js Updates tests to reflect repository-level pagination API.
modules/billing/tests/billing.extraBalance.listLedger.perf.integration.tests.js Adds a perf gate test for slice-based ledger pagination.
modules/billing/tests/billing.plans.unit.tests.js Adds tests for clearPlansCache() and duplicate priceId warning.
modules/billing/tests/billing.meter.service.unit.tests.js Updates tests to validate new getMeterRunBase export.
modules/billing/tests/billing.controller.unit.tests.js Updates expectations for the new withIdempotency(..., ctx) signature and log shape.

Comment on lines +288 to +305
$project: {
_id: 0,
cachedBalance: 1,
total: { $size: '$ledger' },
// Sort descending by `at` then slice the requested page
ledgerPage: {
$slice: [
{
$sortArray: {
input: '$ledger',
sortBy: { at: -1 },
},
},
skip,
limit,
],
},
},
Comment on lines +53 to +68
const start = performance.now();
const result = await BillingExtraBalanceRepository.listLedgerPage(String(orgId), 0, 20);
const elapsed = performance.now() - start;

expect(result).not.toBeNull();
expect(result.total).toBe(1000);
expect(result.ledgerPage).toHaveLength(20);
expect(result.cachedBalance).toBe(1000000);

// Entries should be sorted descending by `at` (newest first)
const firstAt = new Date(result.ledgerPage[0].at).getTime();
const secondAt = new Date(result.ledgerPage[1].at).getTime();
expect(firstAt).toBeGreaterThanOrEqual(secondAt);

// Performance gate: < 50ms for a 1000-entry ledger page fetch
expect(elapsed).toBeLessThan(50);

expect(mockBillingWebhookService.handleSubscriptionUpdated).toHaveBeenCalledWith({ id: 'sub_123' }, eventData);
expect(mockBillingWebhookService.withIdempotency).toHaveBeenCalledWith(eventData, expect.any(Function));
expect(mockBillingWebhookService.withIdempotency).toHaveBeenCalledWith(eventData, expect.any(Function), expect.objectContaining({}));
@@ -195,10 +195,11 @@ describe('Billing webhook idempotency unit tests:', () => {
BillingWebhookService.withIdempotency(event, handler),
).rejects.toThrow('handler blew up');

@@ -404,7 +404,11 @@ describe('withIdempotency — replay-storm dead-letter protection:', () => {

await expect(BillingWebhookService.withIdempotency(event, handler)).rejects.toThrow('transient');

Comment on lines 25 to 32
/**
* @function getMeterRunBase
* @description Resolve the configured base units charged when no costs are present.
* Evaluated lazily on each call so test overrides and runtime config
* changes are always reflected (avoids the stale module-scope const pattern).
* @returns {number} Meter run base units.
*/
export const getMeterRunBase = () =>
…ield

- Add $ifNull(['$ledger', []]) guard in $size and $sortArray expressions
  so listLedgerPage is robust against legacy/partial docs with missing ledger
- Add toHaveBeenCalledTimes(1) assertions before mock.calls[0] access in
  webhook idempotency + hardening tests for clearer failure diagnostics
- Add dedicated controller test asserting req.id is propagated as requestId
  into withIdempotency third arg
@PierreBrisorgueil PierreBrisorgueil merged commit 402a9a5 into master May 6, 2026
7 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the feat/billing-polish-final branch May 6, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants